package org.bhf.json;

/**
 * Copyright (c) 2007, Blue Hole Software. All rights reserved.
 * Code licensed under the Apache 2.0 License:
 * http://www.apache.org/licenses/
 */

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;

import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Map;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Level;

/**
 * This class generates writers for arrays, collections, and pojos. Each writer looks something like...
 * <pre>
 * public final class someGeneratedClassName
 * {
 *     public static void writer( DeclaredType pojo, Writer out, JSONWriter writer )
 *         throw IOException
 *     {
 *         ...
 *     }
 * }
 * </pre>
 */
final class JSONWriterGenerator
{
    public static final int DEPTH_LIMIT = 100;

    private Set<String> typesInProgress = new HashSet<String>();

    /**
     * Resolve a writer class into a method for the given type.
     *
     * @param type Type of object to write
     * @param writerClassName Name of write
     * @return The resolved method
     */
    Method  resolveWriter( final Type type, final String writerClassName )
    {
        try
        {
            final Class<?>  parserClass = Class.forName( writerClassName, true, Thread.currentThread().getContextClassLoader() );
            final Class     clazz       = Util.digRawType( type );

            if( Enum.class.isAssignableFrom( clazz ) )
                return parserClass.getMethod( "write", Enum.class, Writer.class, JSONWriter.class );
            else
                return parserClass.getMethod( "write", clazz, Writer.class, JSONWriter.class );
        }
        catch( ClassNotFoundException cnfe )
        {
            Package.log.log( Level.SEVERE, "Unable to load created writer class " + writerClassName );
            throw new RuntimeException( cnfe );
        }
        catch( NoSuchMethodException e )
        {
            Package.log.log( Level.SEVERE, "Missing write() method on writer " + writerClassName );
            throw new RuntimeException( e );
        }
        catch( UnsupportedTypeException e )
        {
            Package.log.log( Level.SEVERE, "Unsupported type " + type.toString() );
            throw new RuntimeException( e );
        }
    }

    /**
     * Get a parser name for the given type. The result may be a built-in or generated parser, as needed. If
     * the parser needs to be generated, it will be loaded prior to return.
     *
     * @param type The type of object to be read
     * @return Class name of the parser. The method name is always parse, with a signature appropriate to the type.
     * @throws java.io.IOException Write error
     */
    String  getWriter( final Type type )
        throws IOException
    {
        // Try to get it from the class loader first
        final String writerClassName = Util.getWriterClassNameForType( type );

        try
        {
            Class.forName( writerClassName, true, Thread.currentThread().getContextClassLoader() );
            return writerClassName;
        }
        catch( ClassNotFoundException cnfe )
        {
            if( NativeJSONWriters.hasBuiltInSupport( type ) )
            {
                return NativeJSONWriters.class.getName();
            }
            else
            {
                return generateFor( type );
            }
        }
    }

    //
    // Private
    //

    /**
     * Generate a writer for the given type.
     *
     * @param type The type of object to be written
     * @return Class implementing the writer for this type
     * @throws IOException Write error
     */
    private String  generateFor( final Type type )
        throws IOException
    {
        final StringBuffer  body        = new StringBuffer();
            generateForComponent( type, body, "pojo", 0 );
        final Class<?>      writerClass = generateWriterFromMethodBody( type, body.toString() );

        return writerClass.getName();
    }

    //
    // POJOs
    //

    /**
     * Generate code for writing a POJO/JavaBean (property list)
     *
     * @param pojo Class of POJO
     * @param body Code accumulator
     * @param accessor Expression used to access the element: array index, etc.
     * @param depth Nested depth to be used in generating unique variable names
     * @throws IOException Write error
     */
    private void        generateForPOJO( final Class<?> pojo, final StringBuffer body, final String accessor, final int depth )
        throws IOException
    {
        body.append( "{boolean first=true;out.write('{');" );

        // Properties
        final Method[] methods = pojo.getMethods();

        for( final Method method : methods )
        {
            if( Util.isGetter( method ) )
            {
                final String    property        = Util.methodNameToProperty( method );
                final String    excludeKey      = method.getDeclaringClass().getName() + "." + property;
                final boolean   never           = JSONTransient.Util.isTransient( method );

                if( !never )
                {
                    final String nestedAccessor = accessor + "." + method.getName() + "()";
                    final Type valueType = method.getGenericReturnType();

                    body.append( "if(!writer.exclude(\"" ).append( excludeKey ).append( "\")){" ); // If include
                    body.append( "if(!first)out.write(',');first=false;" );
                    body.append( "writer.write(\"" ).append( property ).append( "\",out);" );
                    body.append( "out.write(':');" );
                    if( valueType instanceof Class && Class.class.cast( valueType ).isPrimitive() )
                    {
                        generateForComponent( valueType, body, nestedAccessor, depth + 1 );
                    }
                    else
                    {
                        body.append( "if(" ).append( nestedAccessor ).append( "!=null){" );
                        generateForComponent( valueType, body, nestedAccessor, depth + 1 );
                        body.append( "}else{writer.writeNull(out);}" );
                    }
                    body.append( "}" ); // If include
                }
            }
        }

        body.append( "out.write('}');}" );
    }

    //
    // Collections
    //

    /**
     * Generate code for writing a Map
     *
     * @param map Type of map
     * @param body Code accumulator
     * @param accessor Expression used to access the element: array index, etc.
     * @param depth Nested depth to be used in generating unique variable names
     * @throws IOException Write error
     */
    private void generateForMap( final Type map, final StringBuffer body, final String accessor, final int depth )
        throws IOException
    {
        final Type[]    componentTypess     = Util.getMapComponentTypes( map );
        final Type      keyType             = componentTypess[0];
        final Type      valueType           = componentTypess[1];
        final String    counterName         = "i" + depth; // Name of counter or iterator
        final String    variableName        = "x" + depth; // Name of variable pointing to map
        final String    entryVariableName   = "e" + depth; // Name of variable pointing to map
        final String    keyVariableName     = "k" + depth; // Name of variable pointing to map
        final String    valueVariableName   = "v" + depth; // Name of variable pointing to map
        final String    keyCast             = Util.getDeclarationName( keyType, true );
        final String    valueCast           = Util.getDeclarationName( valueType, true );
        final String    entryCast           = "java.util.Map.Entry";

        body.append( "out.write('{');" );
        body.append( "{" );
        body.append( Util.getDeclarationName( map, true ) ).append( " " ).append( variableName ).append( " = " ).append( accessor ).append( ";" );
        body.append( "boolean first = true;" );
        body.append( "for( java.util.Iterator " ).append( counterName ).append( " = " ).append( variableName ).append( ".entrySet().iterator();" ).append( counterName ).append( ".hasNext();){" );
        body.append( "if( !first ) out.write(','); first = false;" );
        body.append( entryCast ).append( " " ).append( entryVariableName ).append( "=(" ).append( entryCast ).append( ")" ).append( counterName ).append( ".next();" );
        body.append( keyCast ).append( " " ).append( keyVariableName ).append( "=(" ).append( keyCast ).append( ")" ).append( entryVariableName ).append( ".getKey();" );
        body.append( valueCast ).append( " " ).append( valueVariableName ).append( "=(" ).append( valueCast ).append( ")" ).append( entryVariableName ).append( ".getValue();" );

        // Write entry key. Keys must be something that will convert to a String
        body.append( "writer.write( \"\" + " ).append( keyVariableName ).append( ",out);" );
        body.append( "out.write(':');" );

        // Keys cannot be null, so just check the value
        if( valueType instanceof Class && Class.class.cast( valueType ).isPrimitive() )
        {
            generateForComponent( valueType, body, valueVariableName, depth );
            body.append( "}" );
        }
        else
        {
            body.append( "if(" ).append( valueVariableName ).append( "!=null){" );
            generateForComponent( valueType, body, valueVariableName, depth );
            body.append( "}else{writer.writeNull(out);}}" );
        }
        
        body.append( "}" );
        body.append( "out.write('}');" );
    }

    //
    // Collections
    //

    /**
     * Generate code for writing a Collection
     *
     * @param collection Type of collection
     * @param body Code accumulator
     * @param accessor Expression used to access the element: array index, etc.
     * @param depth Nested depth to be used in generating unique variable names
     * @throws IOException Write error
     */
    private void generateForCollection( final Type collection, final StringBuffer body, final String accessor, final int depth )
        throws IOException
    {
        final Type      componentType       = Util.getCollectionComponentType( collection );
        final String    counterName         = "i" + depth; // Name of counter or iterator
        final String    variableName        = "x" + depth; // Name of variable pointing to collection
        final String    innerVariableName   = "y" + depth; // Name of variable pointing to collection
        final String    cast                = Util.getDeclarationName( componentType, true );

        body.append( "out.write('[');" );
        body.append( "{" );
        body.append( Util.getDeclarationName( collection, true ) ).append( " " ).append( variableName ).append( " = " ).append( accessor ).append( ";" );
        body.append( "boolean first = true;" );
        body.append( "for( java.util.Iterator " ).append( counterName ).append( " = " ).append( variableName ).append( ".iterator();" ).append( counterName ).append( ".hasNext();){" );
        body.append( "if( !first ) out.write(','); first = false;" );
        body.append( cast ).append( " " ).append( innerVariableName ).append( "=(" ).append( cast ).append( ")" ).append( counterName ).append( ".next();" );
        if( componentType instanceof Class && Class.class.cast( componentType ).isPrimitive() )
        {
            generateForComponent( componentType, body, innerVariableName, depth );
            body.append( "}" );
        }
        else
        {
            body.append( "if(" ).append( innerVariableName ).append( "!=null){" );
            generateForComponent( componentType, body, innerVariableName, depth );
            body.append( "}else{writer.writeNull(out);}}" );
        }
        body.append( "}" );
        body.append( "out.write(']');" );
    }

    //
    // Arrays
    //

    /**
     * Generate array writing code based on Class
     *
     * @param array Class of array
     * @param body Code accumulator
     * @param accessor Expression used to access the element: array index, etc.
     * @param depth Nested depth to be used in generating unique variable names
     * @throws IOException Write error
     */
    private void    generateForArray( final Class array, final StringBuffer body, final String accessor, final int depth )
        throws IOException
    {
        final Class     componentType   = array.getComponentType();
        final String    counterName     = "i" + depth;
        final String    variableName    = "x" + depth;
        final String    indexer         = variableName + "[" + counterName + "]";

        body.append( "out.write('[');" );
        body.append( "{" );
        body.append( Util.getDeclarationName( array, true ) ).append( " " ).append( variableName ).append( " = " ).append( accessor ).append( ";" );
        body.append( "for( int " ).append( counterName ).append( " = 0; " ).append( counterName ).append( " < " ).append( variableName ).append( ".length; " ).append( counterName ).append( "++ ) {" );
        body.append( "if( " ).append( counterName ).append( " > 0 ) out.write(',');" );
        if( componentType.isPrimitive() )
        {
            generateForComponent( componentType, body, indexer, depth );
            body.append( "}" );
        }
        else
        {
            body.append( "if(" ).append( indexer ).append( "!=null){" );
            generateForComponent( componentType, body, indexer, depth );
            body.append( "}else{writer.writeNull(out);}}" );
        }
        body.append( "}" );
        body.append( "out.write(']');" );
    }

    /**
     * Generate array writing code based on Type
     *
     * @param array Class of array
     * @param body Code accumulator
     * @param accessor Expression used to access the element: array index, etc.
     * @param depth Nested depth to be used in generating unique variable names
     * @throws IOException Write error
     */
    private void    generateForArray( final GenericArrayType array, final StringBuffer body, final String accessor, final int depth )
        throws IOException
    {
        final Type      componentType   = array.getGenericComponentType();
        final String    counterName     = "i" + depth;
        final String    variableName    = "x" + depth;
        final String    indexer         = variableName + "[" + counterName + "]";

        body.append( "out.write('[');" );
        body.append( "{" );
        body.append( Util.getDeclarationName( array, true ) ).append( " " ).append( variableName ).append( " = " ).append( accessor ).append( ";" );
        body.append( "for( int " ).append( counterName ).append( " = 0; " ).append( counterName ).append( " < " ).append( variableName ).append( ".length; " ).append( counterName ).append( "++ ) {" );
        body.append( "if( " ).append( counterName ).append( " > 0 ) out.write(',');" );
        if( componentType instanceof Class && Class.class.cast( componentType ).isPrimitive() )
        {
            generateForComponent( componentType, body, indexer, depth );
            body.append( "}" );
        }
        else
        {
            body.append( "if(" ).append( indexer ).append( "!=null){" );
            generateForComponent( componentType, body, indexer, depth );
            body.append( "}else{writer.writeNull(out);}}" );
        }
        body.append( "}" );
        body.append( "out.write(']');" );
    }

    /**
     * Generate inline code for a given component. This is where all the type switching occurs and the
     * messy mixing of Type and Class.
     *
     * @param componentType Type of component
     * @param body Code accumulator
     * @param indexer Expression used to access the element: array index, etc.
     * @param depth Nested depth to be used in generating unique variable names
     * @throws IOException Write error
     */
    private void    generateForComponent(
        final Type componentType, final StringBuffer body, final String indexer, final int depth
    )
        throws IOException
    {
        final String writerClassName = Util.getWriterClassNameForType( componentType );

        if( !NativeJSONWriters.hasBuiltInSupport( componentType ) )
        {
            if( typesInProgress.contains( writerClassName ) ) // Cyclic class, revert to reflection
            {
                body.append( "try{");
                body.append( "writer.writeObject(" ).append( indexer ).append( ",out);" );
                body.append( "}catch(NullPointerException npe){writer.writeNull(out);}");
                return;
            }
            else
            {
                typesInProgress.add( writerClassName );
            }
        }

        if( depth > DEPTH_LIMIT )
            throw new CyclicGraphException();

        if( componentType instanceof Class )
        {
            final Class clazz = Class.class.cast( componentType );

            if( NativeJSONWriters.hasBuiltInSupport( componentType ) )
            {
                body.append( NativeJSONWriters.class.getName() );
                body.append( ".write(" ).append( indexer ).append( ",out,writer);" );
            }
            else if( clazz.isArray() )
            {
                generateForArray( clazz, body, indexer, depth + 1 );
            }
            else if( Map.class.isAssignableFrom( clazz ) )
            {
                generateForMap( clazz, body, indexer, depth + 1 );
            }
            else if( Collection.class.isAssignableFrom( clazz ) )
            {
                generateForCollection( clazz, body, indexer, depth + 1 );
            }
            else if( Object.class.equals( clazz ) ) // A bit bizarre. Code generator bug + erasure leaves us with just
                                                    // Object when we've lost type info. No choice but reflection.
            {
                body.append( "try{");
                body.append( "writer.writeObject(" ).append( indexer ).append( ",out);" );
                body.append( "}catch(NullPointerException npe){writer.writeNull(out);}");
            }
            else
            {
                generateForPOJO( clazz, body, indexer, depth + 1 );
            }
        }
        else if( componentType instanceof GenericArrayType )
        {
            generateForArray( GenericArrayType.class.cast( componentType ), body, indexer, depth + 1 );
        }
        else if( componentType instanceof ParameterizedType )
        {
            final ParameterizedType pType = ParameterizedType.class.cast( componentType );
            final Type rawType = pType.getRawType();

            if( rawType instanceof Class && Map.class.isAssignableFrom( (Class)rawType ) )
            {
                generateForMap( componentType, body, indexer, depth + 1 );
            }
            else if( rawType instanceof Class && Collection.class.isAssignableFrom( (Class)rawType ) )
            {
                generateForCollection( componentType, body, indexer, depth + 1 );
            }
            else if( rawType instanceof Class )
            {
                generateForPOJO( Class.class.cast( rawType ), body, indexer, depth + 1 );
            }
            else
            {
                body.append( "try{");
                body.append( "writer.writeObject(" ).append( indexer ).append( ",out);" );
                body.append( "}catch(NullPointerException npe){writer.writeNull(out);}");
            }
        }
        else // Object
        {
            throw new UnsupportedTypeException( componentType );
        }
    }

    //
    // Common
    //

    /**
     * Generate a writer, given the body of the write method.
     *
     * @param type The type of object to be written with this writer
     * @param methodBody Will be modified, do not reuse.
     * @return Class implementing the writer for this type
     * @throws IOException Write error
     */
    private Class<?> generateWriterFromMethodBody( final Type type, final String methodBody )
        throws IOException
    {
        final ClassPool pool = Util.getClassPool();
        // Open class
        final CtClass writer = pool.makeClass( Util.getWriterClassNameForType( type ) );

        // Open method
        final StringBuffer body = new StringBuffer();

        body.append( "public static void write(" ).
            append( Util.getDeclarationName( type, true ) ).
            append( " pojo,java.io.Writer out, org.bhf.json.JSONWriter writer) throws java.io.IOException {" );
        body.append( methodBody );
        body.append( "}" );
        try
        {
//            System.out.println( body.toString() );
            // Close method
            {
                final CtMethod write = CtNewMethod.make( body.toString(), writer );
                writer.addMethod( write );
            }

            // Close class
            return writer.toClass();
        }
        catch( CannotCompileException e )
        {
            System.out.println( body.toString() );
            throw new RuntimeException( e ); // Soften because a compile error implies a problem with our generated code
        }
    }
}
